import java.util.concurrent.BlockingQueue;
import java.util.concurrent.PriorityBlockingQueue;
import java.util.concurrent.TimeUnit;

/**
 * 自定义任务类
 */
class MyTask implements Comparable<MyTask> {

    private Runnable runnable;

    private long time;

    public MyTask(Runnable runnable, long time) {
        if (runnable == null) {
            throw new IllegalArgumentException("任务不能为空~");
        }
        if (time < 0) {
            throw new RuntimeException("时间不能为负数~");
        }
        this.runnable = runnable;
        this.time = time + System.currentTimeMillis();
    }

    public Runnable getRunnable() {
        return runnable;
    }

    public long getTime() {
        return time;
    }

    @Override
    public int compareTo(MyTask o) {
        if (this.time > o.time) {
            return 1;
        } else if (this.time < o.time) {
            return -1;
        } else {
            return 0;
        }
    }
}

/**
 * 自定义定时器
 */
public class MyTimer {

    private BlockingQueue<MyTask> blockingQueue = new PriorityBlockingQueue<>();
    Object locker = new Object();

    public MyTimer() {

        // 任务线程
        Thread thread = new Thread(() -> {
           while (true) {
               try {

                   // 2. 要是我们直接在头顶上加锁呢, 在 take 方法前面加上锁, 这个也是不可行的, 因为当我们的队列中没有
                   // 任务的时候, 我们在while寻找任务, 就会发现, 这个 thread 拿上了 locker 的锁, 然后他还在阻塞等待
                   // 这就导致了后面的加任务拿不到锁, 也在等待, 这就形成了死锁....
                   // 所以这种改发也是不可行的
//                   synchronized (locker) {
//                       ...
//                   }

                   MyTask myTask = blockingQueue.take();

                   long currentTime = System.currentTimeMillis();

                   if (myTask.getTime() > currentTime) {

                       long waitTime = myTask.getTime() - currentTime;
                       blockingQueue.put(myTask);

                       // 1. 要是在这里加锁, 那么因为 CPU 的随机调度, 在我们刚获取一个任务的时候, 可能又新加了一个任务进来
                       // 这个时候的等待时间可能会与刚来的这个任务有冲突, 导致我们的这个新任务他执行不了, 需要改进
//                       synchronized(locker) {
//                           locker.wait(waitTime);
//                       }

                       // 3.最终还是在这里加锁, 但是是在有后台线程的情况下
                       synchronized(locker) {
                           locker.wait(waitTime);
                       }

                   } else {

                       myTask.getRunnable().run();
                   }

               } catch (InterruptedException e) {
                   e.printStackTrace();
               }
           }
        });

        // 启动
        thread.start();

        // 3. 那怎么解决上面的问题呢? 其实在处理任务无法及时处理的问题时, 扩大了加锁的范围, 却又引入了更大的问题. 一般我们两害相全取其轻
        // 可以创建一个后台扫描线程, 只做唤醒操作, 定时一秒或者10毫秒, 唤醒一次, 虽然这个方法也会有问题的存在, 但是已经是可以解决这个问题
        // 所需的最小代价了
        // 创建一个后台线程
        Thread daemonThread = new Thread(() -> {
            while (true) {

                synchronized (locker) {
                    locker.notifyAll();
                }

                // 休眠一会
                try {
                    TimeUnit.MICROSECONDS.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        daemonThread.setDaemon(true);
        daemonThread.start();
    }

    public void schedule(Runnable runnable, long time) {
        try {

            MyTask myTask = new MyTask(runnable, time);

            // 这里并不是 put 了, 就将任务加入 队列中了, 而是要等待该方法执行结束了
            // 之后才将任务加入队列, put 因为后续可能还有代码, 涉及到一些啥啥的判断
            // 那么要是有问题的话, 这个任务不就是个家任务了吗, 所以才是方法结束之后
            // 才将任务加入队列
            // 所以在 take 之前加锁会造成死锁
            blockingQueue.put(myTask);

            synchronized (locker) {
                locker.notifyAll();
            }

        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) {
        MyTimer myTimer = new MyTimer();
        myTimer.schedule(() -> {
            System.out.println("6:30, 起床~");
        }, 0);

        myTimer.schedule(() -> {
            System.out.println("7:00, 晨跑~");
        }, 1000);

        myTimer.schedule(() -> {
            System.out.println("7:30, 吃饭~");
        }, 2000);

        myTimer.schedule(() -> {
            System.out.println("8:00, 学习~");
        }, 3000);

        myTimer.schedule(() -> {
            System.out.println("23:30, 睡觉~");
        }, 4000);
    }
}
